package com.seanchenxi.gwt.xmlrpc.client.util;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;

import com.google.gwt.xml.client.Document;
import com.google.gwt.xml.client.Element;
import com.google.gwt.xml.client.Node;
import com.google.gwt.xml.client.Text;
import com.google.gwt.xml.client.XMLParser;
import com.seanchenxi.gwt.xmlrpc.client.XmlRpcException;
import com.seanchenxi.gwt.xmlrpc.client.XmlRpcRequest;
import com.seanchenxi.gwt.xmlrpc.client.model.BaseModel;

/**
 * The Class XmlRpcParser.
 */
public class XmlRpcParser extends XmlParser implements HasXmlRpcConstant {

	/**
	 * Write request data.
	 * 
	 * @param xmlRpcRequest
	 *            the xml rpc request
	 * @return the XML Request Data
	 */
	public static String writeRequestData(XmlRpcRequest<?> xmlRpcRequest) {
		Document doc = XMLParser.createDocument();

		Element methodCallEl = doc.createElement(METHOD_CALL);
		Element methodNameEl = doc.createElement(METHOD_NAME);
		Element paramsEl = doc.createElement(PARAMS);

		Text methodNameTxt = doc.createTextNode(xmlRpcRequest.getMethodName());

		for (int i = 0; i < xmlRpcRequest.getParameterCount(); i++) {
			addParamElement(paramsEl, xmlRpcRequest.getParameter(i));
		}

		methodNameEl.appendChild(methodNameTxt);
		methodCallEl.appendChild(methodNameEl);
		methodCallEl.appendChild(paramsEl);
		doc.appendChild(methodCallEl);

		return doc.toString();
	}

	/**
	 * Adds the parameter element in the given "params" element .
	 * 
	 * @param paramsEl
	 *            the given "params" element
	 * @param param
	 *            the parameter object
	 */
	private static void addParamElement(Element paramsEl, Object param) {
		Document doc = paramsEl.getOwnerDocument();
		Element valueEl = createValueElement(doc, param);
		if (valueEl != null) {
			Element paramEl = doc.createElement(PARAM);
			paramEl.appendChild(valueEl);
			paramsEl.appendChild(paramEl);
		}
	}

	/**
	 * create the value element.
	 * 
	 * @param doc
	 *            the Xml document object
	 * @param param
	 *            the parameter
	 * @return the value element
	 */
	private static Element createValueElement(Document doc, Object param) {

		Element valueEl = doc.createElement(VALUE);
		Element valueTypeEl = null;

		if (param == null) {
			valueTypeEl = doc.createElement(NULL);
		} else if (param instanceof String) {
			valueTypeEl = doc.createElement(STRING);
			valueTypeEl.appendChild(doc.createTextNode(param.toString()));
		} else if (param instanceof Integer) {
			valueTypeEl = doc.createElement(INT);
			valueTypeEl.appendChild(doc.createTextNode(param.toString()));
		} else if (param instanceof Short) {
			valueTypeEl = doc.createElement(I4);
			valueTypeEl.appendChild(doc.createTextNode(param.toString()));
		} else if (param instanceof Double || param instanceof Float) {
			valueTypeEl = doc.createElement(DOUBLE);
			valueTypeEl.appendChild(doc.createTextNode(param.toString()));
		} else if (param instanceof Boolean) {
			String text = ((Boolean) param).booleanValue() ? "1" : "0";
			valueTypeEl = doc.createElement(BOOLEAN);
			valueTypeEl.appendChild(doc.createTextNode(text));
		} else if (param instanceof BaseModel) {
			valueEl = doc.createElement(STRUCT);
			HashMap<String, Object> struct = ((BaseModel) param)
					.getProperties();
			for (String key : struct.keySet()) {
				Element memberNameEl = doc.createElement(NAME);
				memberNameEl.appendChild(doc.createTextNode(key));
				Element memberEl = doc.createElement(MEMBER);
				memberEl.appendChild(memberNameEl);
				memberEl.appendChild(createValueElement(doc, struct.get(key)));
				valueEl.appendChild(memberEl);
			}
		}

		if (valueTypeEl != null) {
			valueEl.appendChild(valueTypeEl);
		}

		return valueEl;
	}

	/**
	 * Parser value node.
	 * 
	 * @param node
	 *            the node
	 * @return the value object
	 * @throws XmlRpcException
	 *             the xml rpc exception
	 */
	public static Object parserValueNode(Node node) throws XmlRpcException {

		if (firstElementChild(node) == null) {
			String strValue = textValueOf(node);
			return (strValue == null ? "" : strValue);
		}

		Node valueType = firstElementChild(node);

		if (isEqual(valueType, I4) || isEqual(valueType, INT)) {
			String intValue = textValueOf(valueType);
			if (intValue == null)
				throw new XmlRpcException("Integer child is not a text node");
			try {
				return Integer.valueOf(intValue);
			} catch (NumberFormatException e) {
				throw new XmlRpcException("Value \"" + intValue
						+ "\" can not be cast to" + " an integer");
			}
		} else if (isEqual(valueType, BOOLEAN)) {
			String boolValue = textValueOf(valueType);
			if ("0".equals(boolValue)) {
				return Boolean.FALSE;
			} else if ("1".equals(boolValue)) {
				return Boolean.TRUE;
			} else {
				throw new XmlRpcException("Value \"" + boolValue
						+ "\" must be 0 or 1");
			}
		} else if (isEqual(valueType, STRING)) {
			String strValue = textValueOf(valueType);
			return (strValue == null ? "" : strValue);
		} else if (isEqual(valueType, DOUBLE)) {
			String doubleValue = textValueOf(valueType);
			if (doubleValue == null)
				throw new XmlRpcException(
						"Child of <double> is not a text node");
			try {
				return Double.valueOf(doubleValue);
			} catch (NumberFormatException e) {
				throw new XmlRpcException("Value \"" + doubleValue
						+ "\" is not a double");
			}
		} else if (isEqual(valueType, DATETIME_ISO8601)) {

			return convert(valueType);

		} else if (isEqual(valueType, BASE64)) {
			// TODO convert this
		} else if (isEqual(valueType, STRUCT)) {
			
			int totalEl = valueType.getChildNodes().getLength();
			HashMap<String, Object> map = new HashMap<String, Object>(totalEl);

			for (int i = 0; i < totalEl; i++) {
				if (!isElement(valueType.getChildNodes().item(i))) {
					continue;
				}

				Element memberNode = (Element) valueType.getChildNodes().item(i);
				String name = null;
				Object value = null;

				if (!isEqual(memberNode, MEMBER))
					throw new XmlRpcException(
							"Children of <struct> can only be named <member>");
				// NodeList.getElementsByTagName(String) does a deep search, so
				// we
				// can legally get more than one back. Therefore, if the
				// response
				// has more than one <name/> and <value/> at it's highest level,
				// we will only process the first one it comes across.
				if (memberNode.getElementsByTagName(NAME).getLength() < 1)
					throw new XmlRpcException(
							"A <struct> element must contain "
									+ "at least one <name> child");
				if (memberNode.getElementsByTagName(VALUE).getLength() < 1)
					throw new XmlRpcException(
							"A <struct> element must contain "
									+ "at least one <value> child");

				name = textValueOf(memberNode.getElementsByTagName(NAME)
						.item(0));
				value = parserValueNode(memberNode
						.getElementsByTagName(VALUE).item(0));
				if (name == null)
					throw new XmlRpcException("The <name> element must "
							+ "contain a string value");
				map.put(name, value);
			}

			return map;

		} else if (isEqual(valueType, ARRAY)) {
			Node dataNode = firstElementChild(valueType);
			if (!isEqual(dataNode, DATA))
				throw new XmlRpcException(
						"An <array> element must contain a single <data> element");

			ArrayList<Object> list = new ArrayList<Object>();
			for (int i = 0; i < dataNode.getChildNodes().getLength(); i++) {
				Node valueNode = dataNode.getChildNodes().item(i);
				if (valueNode.getNodeType() != Node.ELEMENT_NODE)
					continue;
				if (!isEqual(valueNode, VALUE))
					throw new XmlRpcException(
							"Children of <data> may only be <value> elements");
				list.add(parserValueNode(valueNode));
			}

			return list;

		} else if (isEqual(valueType, NULL)) {
			return null;
		}

		throw new XmlRpcException(
				"Improper XML-RPC response format: Unknown node name \""
						+ valueType.getNodeName() + "\" in " + node.toString());
	}

	/**
	 * Gets the value node.
	 * 
	 * @param responseText
	 *            the response text
	 * @return the value node
	 * @throws XmlRpcException
	 *             the xml rpc exception
	 */
	public static Element getValueNode(String responseText)
			throws XmlRpcException {

		Document doc = XMLParser.parse(responseText);

		Element respEl = doc.getDocumentElement();

		if (isEqual(respEl, METHOD_RESPONSE) && hasElementChild(respEl)) {
			Element paramsEl = firstElementChild(respEl);
			if (isEqual(paramsEl, PARAMS) && hasElementChild(paramsEl)) {
				Element paramEl = firstElementChild(paramsEl);
				if (isEqual(paramEl, PARAM) && hasElementChild(paramEl)) {
					Element valueEl = firstElementChild(paramEl);
					if (isEqual(valueEl, VALUE)) {
						return valueEl;
					} else {
						throw new XmlRpcException(
								"Value node must be named <value>, not " + "<"
										+ valueEl.getNodeName() + ">");
					}
				}
			}
		}

		return null;
	}

	/**
	 * Parser response.
	 * 
	 * @param responseText
	 *            the response text
	 * @return the object
	 * @throws XmlRpcException
	 *             the xml rpc exception
	 */
	public static Object parserResponse(String responseText)
			throws XmlRpcException {
		Node node = getValueNode(responseText);
		return parserValueNode(node);
	}

	/**
	 * Convert the iso8601 data time to java Date object
	 * 
	 * @param dateTime_iso8601
	 *            the iso8601 date time 
	 * @return the date
	 */
	@SuppressWarnings("deprecation")
	private static Date convert(Node dateTime_iso8601) {

		String dateString = textValueOf(dateTime_iso8601);
		String[] dateTime = dateString.split("T");
		String date = dateTime[0];
		String time = dateTime[1];

		int year = 0, month = 0, day = 0;
		year = Integer.parseInt(date.substring(0, 4));
		month = Integer.parseInt(date.substring(4, 6));
		day = Integer.parseInt(date.substring(6, 8));

		int hrs = 0, min = 0, sec = 0;
		String[] times = time.split(":");
		hrs = Integer.parseInt(times[0]);
		min = Integer.parseInt(times[1]);
		sec = Integer.parseInt(times[2]);

		Date realDate = new Date();
		realDate.setYear(year - 1900);
		realDate.setMonth(month - 1);
		realDate.setDate(day);
		realDate.setHours(hrs);
		realDate.setMinutes(min);
		realDate.setSeconds(sec);
		realDate.setTime((realDate.getTime() / 1000) * 1000);

		return realDate;
	}
}
